# -*- coding: utf-8 -*-
'''
Created on 13.08.2014

@author: mathias
'''
import os, datetime, sys, time, thread
from ImageHelper import GetImageTimeStamp, GetOrAddKey, GetEXIF, SetEXIF, \
    AutoOrientImage, SetOrAddKey
from ImageHelper import CanAutoOrient
from ImageHelper import IsImageFile
import Image
from StringIO import StringIO


class MediaFiles(object):
    _media_file_db = {}
    _access_lock = thread.LockType
    _access_lock = thread.allocate_lock()

    def __init__(self, files=[], incr_callback=None):
        '''
        def incr_callback(media_class, file_name, number)
        '''
        self._local_files = []
        self._min_dt = datetime.datetime.max
        self._max_dt = datetime.datetime.min
        if files:
            n = len(files)
            for f in files:
                n -= 1
                if self.append(f) and incr_callback:
                    try:
                        if incr_callback(self, f, n) != None:
                            # abort, when function has a result
                            break
                    except:
                        sys.stderr.write('MediaFiles.__init__(): %s --> %s\n' % (f, sys.exc_value))

    def append(self, filename):
        '''append(filename) --> True/False
        Adds a new or update a media file (True) to the internal list, if the format is supported, otherwise result is False.
        '''
        f = filename
        if IsSupportedFile(f):
            if not f in self._local_files:
                self._local_files.append(f)
            # get file modification time
            mdt = GetFileTimeStamp(f)
            update = True
            # append to list
            self._access_lock.acquire()
            info = {}
            if f in self._media_file_db:
                info = self._media_file_db[f]
                # check if file was modified
                if info['modified'] == mdt:
                    # nothing changed
                    update = False
            else:
                self._media_file_db.setdefault(f, info)
            self._access_lock.release()
            # update modified time stamp
            SetOrAddKey(info, 'modified', mdt)
            # fill info fields
            if update:
                # check media type
                if IsImageFile(f):
                    exif = GetEXIF(f)
                    orientation = 0
                    if exif:
                        try:
                            exif_img = exif['Exif']['Image']
                            try:
                                SetOrAddKey(info, 'model', '/'.join((exif_img['Make'], exif_img['Model'])))
                            except:
                                sys.stderr.write('MediaFiles.append(): %s --> %s\n' % (f, sys.exc_value))
                            try:
                                SetOrAddKey(info, 'datetime', exif_img['DateTime'])
                                SetOrAddKey(info, 'hasEXIF', True)
                                orientation = exif_img['Orientation']
                            except:
                                sys.stderr.write('MediaFiles.append(): %s --> %s\n' % (f, sys.exc_value))
                        except:
                            pass
                    # determine image size
                    try:
                        im = Image.open(f, 'r')
                        im = AutoOrientImage(im, orientation)
                        SetOrAddKey(info, 'width', im.size[0])
                        SetOrAddKey(info, 'height', im.size[1])
                        im = None
                        pass
                    except:
                        sys.stderr.write('MediaFiles.append(): %s --> %s\n' % (f, sys.exc_value))
                else:
                    mi = GetMediaInfo(f)
                    if mi:
                        for topic in mi:
                            for key in mi[topic]:
                                val = mi[topic][key]
                                if ' date' in key and type(val) == datetime.datetime:
                                    SetOrAddKey(info, 'datetime', val)
                                    SetOrAddKey(info, 'hasMediaInfo', True)
                                    break
                            if 'datetime' in info:
                                break
            if not 'datetime' in info:
                SetOrAddKey(info, 'datetime', GetFileTimeStamp(f))
            if info['datetime'] < self._min_dt:
                self._min_dt = info['datetime']
            if info['datetime'] > self._max_dt:
                self._max_dt = info['datetime']
            return info
        return None

    def attachData(self, filename, key='data', data=None):
        result = False
        self._access_lock.acquire()
        if filename in self._local_files:
            info = self._media_file_db[filename]
            SetOrAddKey(info, key, data)
            result = True
        self._access_lock.release()
        return result

    def getFiles(self):
        return self._local_files

    def remove(self, file_name):
        try:
            self._local_files.remove(file_name)
        except:
            pass

    def getData(self, filename, key='data'):
        """
        :rtype : object
        """
        result = None
        self._access_lock.acquire()
        if filename in self._local_files:
            info = self._media_file_db[filename]
            if key in info:
                result = info[key]
        self._access_lock.release()
        return result

    def getGroupedByDate(self, onlyFileTime=False):
        """getGroupedByDate(onlyFileTime=False) --> return files sorted by time stamp

            onlyFileTime=False:   time stamp from meta data (EXIF or mediainfo [if installed])
            onlyFileTime=True:    modification date of file
        """
        dts = {}
        self._access_lock.acquire()
        for f in self._local_files:
            try:
                if onlyFileTime:
                    dt = self._media_file_db[f]['modified']
                else:
                    dt = self._media_file_db[f]['datetime']
                dt_val = GetOrAddKey(dts, dt, [])
                dt_val.append(f)
                dt_val.sort()
            except:
                pass
            pass
        self._access_lock.release()
        return dts

    def getByCameraModel(self, model):
        if model is not None and not isinstance(model, basestring):
            raise TypeError('Argument model of wrong type, expected str or None instead %s' % type(model))
        files = []
        self._access_lock.acquire()
        for file_name in self._local_files:
            try:
                if ((model is None and (
                            not 'model' in self._media_file_db[file_name] or self._media_file_db[file_name][
                            'model'] is None))
                    or (model is not None and model.lower() == self._media_file_db[file_name]['model'].lower())
                ):
                    files.append(file_name)
            except:
                pass
            pass
        self._access_lock.release()
        return files

    def count(self):
        result = len(self._local_files)
        return result

    def getDateSorted(self, onlyFileTime=False):
        '''getDateSorted(onlyFileTime=False) --> return files sorted by time stamp
        
            onlyFileTime=False:   time stamp from meta data (EXIF or mediainfo [if installed])
            onlyFileTime=True:    modification date of file
        '''
        result = []
        dts = self.getGroupedByDate(onlyFileTime)
        keys = dts.keys()
        keys.sort()
        for k in keys:
            for f in dts[k]:
                result.append(f)
        return result

    def updateTimeStamp(self, new_dt):
        '''updateTimeStamp(dt)
        dt can be datetime.datetime or datetime.timedelta.
        
        Sets a new info time stamp and, if possible, updates EXIF data.
        '''
        self._access_lock.acquire()
        for f in self._local_files:
            info = self._media_file_db[f]
            if type(new_dt) == datetime.timedelta:
                info['datetime'] += new_dt
            elif type(new_dt) == datetime.timedelta:
                info['datetime'] = new_dt
            else:
                raise TypeError(
                    'Argument new_dt has a wrong data type (%s), expected datetime.datetime or datetime.timedelta' % type(
                        new_dt))
            # commit to EXIF and file time stamp
            if IsImageFile(f):
                SetEXIF(f, ('Exif', 'Image', 'DateTime'), info['datetime'])
            SetFileTimeStamp(f, info['datetime'])
        self._access_lock.release()
        pass

    def getInfo(self, filename):
        result = None
        self._access_lock.acquire()
        fok = filename in self._local_files
        if fok:
            result = self._media_file_db[filename]
        self._access_lock.release()
        # append
        if not fok:
            result = self.append(filename)
        return result

    def getMinDate(self):
        result = self._min_dt
        return result

    def getMaxDate(self):
        result = self._max_dt
        return result

    def get_datetime(self, filename):
        """
        :param filename:basestring
        :return: None or datetime.datetime from meta data of the file
        :rtype: datetime.datetime
        """
        return self.getData(filename, 'datetime')

    def get_modified(self, filename):
        """
        :param filename:basestring
        :return: None or datetime.datetime from file time stamp
        :rtype: datetime.datetime
        """
        return self.getData(filename, 'modified')

    def get_model(self, filename):
        """
        :param filename:
        :return: make and model as string
        :rtype: basestring
        """
        model = self.getData(filename, 'model')
        if model is None:
            model = ''
        return model

    def get_size(self, filename):
        """
        :param filename:
        :return: tuple with width, height or None, None
        :rtype: (int, int)
        """
        w = self.getData(filename, 'width')
        h = self.getData(filename, 'height')
        if w is None or h is None:
            return None, None
        return w, h



def GetMediaInfo(fname, offset_to_local_timeZone=datetime.timedelta(0, 0)):
    topic = "General"
    result = {topic: {}}
    proc = os.popen('mediainfo "%s"' % fname)
    for line in proc.readlines():
        line = line.strip()
        table = line.split(' : ')
        tag = table[0].strip()
        if len(table) == 1 and len(tag) > 0:
            if tag != topic:
                topic = tag
                result.setdefault(topic, {})
        elif len(table) >= 2:
            value = table[1].strip()
            if ' date' in tag:
                try:
                    value = datetime.datetime.strptime(value, '%Z %Y-%m-%d %H:%M:%S')
                    value += offset_to_local_timeZone
                except:
                    pass
            result[topic].setdefault(tag, value)
    return result


def GetDateFromMediaInfo(mediaInfo):
    for topic in mediaInfo:
        for tag in mediaInfo[topic]:
            if " date" in tag:
                if type(mediaInfo[topic][tag]) is datetime.datetime:
                    return mediaInfo[topic][tag]
    return None


def GetMediaTimeStamp(fname):
    '''GetMediaTimeStamp(fname) --> datetime.datetime time stamp
    
    This method can be used for images, movies and audio files.
    '''
    dt = GetDateFromMediaInfo(GetMediaInfo(fname))
    if dt == None:
        dt = GetImageTimeStamp(fname)
    return dt


def GetFileTimeStamp(fname):
    return datetime.datetime.fromtimestamp(os.path.getmtime(fname))


def SetFileTimeStamp(fname, new_datetime=datetime.datetime.now()):
    timestamp = time.mktime(new_datetime.timetuple())
    os.utime(fname, (timestamp, timestamp))
    return GetFileTimeStamp(fname)


def GroupMediaByDateTime(media_files=[]):
    '''GroupMediaByDateTime(media_files) --> result is a dictionary with datetime.datetime as key and file list as value.
    
    This method groups file time stamps by date and time.
    '''
    dt_files = {}
    for f in media_files:
        if IsSupportedFile(f):
            dt = GetMediaTimeStamp(f)
            if type(dt) == datetime.datetime:
                dt_value = GetOrAddKey(dt_files, dt, [])
                dt_value.append(f)
                dt_value.sort()
            pass
        pass
    return dt_files


def SortedFileOutput(srcFiles, fileNameFormat='%(count)4.4d',
                     sub_folder='%(count)3.3d_%(wday)s_%(day)2.2d-%(month)2.2d-%(year)4.4d', series=True,
                     onlyFileTime=False):
    """srcFiles: List of files to process
    Place holder:   %(count) - 1 to n file resp. folder,
                    %(model) - camera model name e.g. 'Sony_LT15i',
                    %(year) %(month) %(day) %(wday) %(hour) %(minute) %(second) - time stamp of file
                    
    fileNameFormat: e.g. %(count)4.4d_%(day)2.2d-%(month)2.2d-%(year)4.4d_%(hour)2.2dh%(minute)2.2dm
                        --> results in: 0001_05-03-2014_10h38m.jpg ... and so on
                        
    sub_folder: e.g. %(count)3.3d_%(wday)s_%(day)2.2d-%(month)2.2d-%(year)4.4d_%(model)s
                        --> results in: 001_Mi_05-03-2014_Sony_LT15i
    """

    destformat = {
        'count': None,
        'year': None,
        'month': None,
        'wday': None,
        'day': None,
        'hour': None,
        'minute': None,
        'second': None,
        'model': None}

    # get sorted file names and time stamps
    mediaDb = MediaFiles(srcFiles, None)
    file_names = mediaDb.getDateSorted(onlyFileTime=onlyFileTime)

    # series cache
    seriesCache = {}
    modelCache = {}

    def getTimeStamp(file_name):
        if onlyFileTime:
            return mediaDb.getData(file_name, 'modified')
        else:
            return mediaDb.getData(file_name, 'datetime')

    # sub function for series group cache
    def getSeriesPath(file_name, new_file_path):
        if not series:
            return None

        newGroupPath = os.path.splitext(new_file_path)[0]
        dt = getTimeStamp(file_name)
        model = mediaDb.getData(file_name, 'model')

        # fill or load from model cache
        modelFiles = GetOrAddKey(modelCache, model, [])
        if len(modelFiles) == 0:
            modelFiles.extend(mediaDb.getByCameraModel(model))

        # check if file already grouped
        seriesGroups = GetOrAddKey(seriesCache, model, {})
        for group in seriesGroups:
            if file_name in seriesGroups[group]:
                return group

        # scan if file is part of an image series
        groupFiles = []
        for nxtFile in modelFiles:
            nxtDt = getTimeStamp(nxtFile)
            if (nxtDt <= dt + datetime.timedelta(seconds=1)) and (nxtDt >= dt - datetime.timedelta(seconds=1)):
                dt = nxtDt
                groupFiles.append(nxtFile)
            else:
                break
            pass

        # check group count
        if len(groupFiles) > 1:
            # add group
            seriesGroups.setdefault(newGroupPath, groupFiles)
            return newGroupPath

        return None


    # initialize lists and counter
    convertList = []
    subFolderList = []
    n = 0

    # scan files
    for f in file_names:

        # initialize time stamp information
        dt = getTimeStamp(f)
        destformat['year'] = dt.year;
        destformat['month'] = dt.month;
        destformat['monthname'] = dt.strftime('%b');  # month name short
        destformat['day'] = dt.day;
        destformat['wday'] = dt.strftime('%a');  # week day name short
        destformat['yday'] = dt.strftime('%j');  # day of the year
        destformat['hour'] = dt.hour;
        destformat['minute'] = dt.minute;
        destformat['second'] = dt.second;

        # count files
        n += 1

        # fill in file specific information
        model = mediaDb.getData(f, 'model')
        if not isinstance(model, basestring):
            model = 'No_Name'
        else:
            model = model.replace(os.path.sep, '_').replace('/', '_').replace('\\', '_').strip().replace(' ', '_')
            model = os.path.normpath(model)
        destformat['model'] = model

        # initialize path element list
        destPathList = []

        # format sub folder
        if sub_folder:
            destformat['count'] = len(subFolderList);
            subfn = sub_folder % destformat
            if not subfn in subFolderList:
                # reformat
                destformat['count'] = len(subFolderList) + 1;
                subfn = sub_folder % destformat
                # append sub folder to list
                subFolderList.append(subfn)
            # append to path
            destPathList.append(subfn)

        # format file name
        ext = os.path.splitext(f)
        if len(ext) < 1:
            ext = ''
        else:
            ext = ext[-1]
        destformat['count'] = n;
        newfn = (fileNameFormat % destformat) + ext
        # append to path
        destPathList.append(newfn)

        # complete path
        imgDstPath = os.path.sep.join(destPathList)

        # check if file is part of a image series
        imgGroupPath = getSeriesPath(f, imgDstPath)
        if imgGroupPath and isinstance(imgGroupPath, basestring):
            # redirect image path to series group path
            imgDstPath = os.path.join(imgGroupPath, newfn)

        # append to convert list
        convertList.append((f, imgDstPath))

    return convertList


def ExecuteConvertList(dest_path, conver_list=[], sym_link=False, auto_orient=True, incr_callback=None):
    '''ExecuteConvertList() -->  result: tuple with count of OK and failed operations (ok, failed)
    
    incr_callback:
        def incr_callback(file_name, element_nbr, dest_name, successful)
    '''

    if sym_link or not CanAutoOrient():
        auto_orient = False

    ok = 0
    failed = 0

    for src, dst in conver_list:

        dstPath = os.path.join(dest_path, os.path.dirname(dst))
        dst_file = os.path.basename(dst)

        pathOk = True
        if not os.path.isdir(dstPath):
            logmsg = "Create folder: %s" % dstPath
            sys.stdout.write(logmsg)
            try:
                os.makedirs(dstPath)
            except:
                sys.stderr.write('Exception: %s' % sys.exc_value)

            if not os.path.exists(dstPath):
                sys.stdout.write(" - FAILED\n")
                failed += 1
                pathOk = False
            else:
                sys.stdout.write(" - OK\n")

        wasOk = False
        if pathOk:
            # start copy/link
            logmsg = "Processing: %s --> %s\n" % (src, dst)
            sys.stdout.write(logmsg)

            # differentiate between OS variants
            if os.name == 'nt':
                s = 'copy /B /V /Y "%s" "%s"' % (src, dstPath + os.path.sep + dst_file)
                s = s.encode('cp1252')
            else:
                if sym_link:
                    relSrc = os.path.relpath(os.path.dirname(src), dstPath)
                    src = relSrc + os.path.sep + os.path.basename(src)
                    s = 'ln -s "%s" "%s"' % (src, dstPath + os.path.sep + dst_file)
                else:
                    s = 'cp -a "%s" "%s"' % (src, dstPath + os.path.sep + dst_file)

            # execute
            status = os.system(s)
            if status == 0:
                wasOk = True
                sys.stdout.write(" - OK\n")
                if auto_orient and IsImageFile(dstPath + os.path.sep + dst_file):
                    back_dt = GetFileTimeStamp(dstPath + os.path.sep + dst_file)
                    s = 'mogrify -auto-orient "%s"' % (dstPath + os.path.sep + dst_file)
                    if os.name == 'nt':
                        s = s.encode('cp1252')
                    status = os.system(s)
                    SetFileTimeStamp(dstPath + os.path.sep + dst_file, back_dt)
                    if status == 0:
                        sys.stdout.write('mogrify -auto-orient - successful')
                    else:
                        sys.stderr.write('mogrify -auto-orient - FAILED (%s)' % status)
                ok += 1
                pass
            else:
                sys.stdout.write(" - FAILED\n")
                sys.stderr.write("%s - ERROR %s\n" % (s, status))
                failed += 1
                pass

        # callback
        if incr_callback != None:
            try:
                abort = incr_callback(src, (ok + failed - 1), dstPath + os.path.sep + dst_file, wasOk)
                if abort != None:
                    sys.stdout.write("Abort by callback result %s\n" % abort)
                    break
            except:
                pass
        pass

    return (ok, failed)


def IsSupportedFile(fname):
    return IsMovieFile(fname) or IsImageFile(fname)


def IsMovieFile(fname):
    if not os.path.isfile(fname): return False
    fsplit = os.path.splitext(fname)
    fext = fsplit[-1].lower()
    if (fext == '.mp4'
        or fext == '.avi'
        or fext == '.mpeg'
        or fext == '.mpg'
        or fext == '.mov'
        or fext == '.ogg'
        or fext == '.ogv'): return True
    pass


def GetTimeStampFromFileName(fname, format="%Y%m%d_%H%M%S"):
    fn = os.path.split(fname)[-1].strip()
    fn = os.path.splitext(fn)[0].strip()
    return datetime.datetime.strptime(fn, format)


import subprocess


def DefaultOpen(url_file):
    """
    Open given file with default user program.
    """
    ret_code = 0
    if sys.platform.startswith('linux'):
        ret_code = subprocess.call(['xdg-open', url_file])
    elif sys.platform.startswith('darwin'):
        ret_code = subprocess.call(['open', url_file])
    elif sys.platform.startswith('win'):
        uri_path = url_file.split('://')[-1]
        s = 'cmd /C "%s"' % uri_path
        ret_code = os.system(s.encode('cp1252'))
    return ret_code


if __name__ == '__main__':
    # path = r'C:\Users\Public\Pictures\Sample Pictures'
    path = r'C:\Users\MathiasAr\Pictures\DCIM'
    imfiles = []
    for f in os.listdir(path):
        f = os.path.join(path, f)
        imfiles.append(f)
        pass

    mf = MediaFiles(imfiles)
    print mf._media_file_db
    print mf.count()
    print mf.getGroupedByDate()

    convertList = SortedFileOutput(imfiles,
                                   '%(count)4.4d_%(day)2.2d-%(month)2.2d-%(year)4.4d_%(hour)2.2dh%(minute)2.2dm',
                                   '%(count)3.3d_%(wday)s_%(day)2.2d-%(month)2.2d-%(year)4.4d_%(model)s', True)
    if convertList != None:
        for src, dst in convertList:
            print src + " --> " + dst

    print "-------------------------"
    print "EXECUTE"
    print "-------------------------"

    def _callback(file_name, number, dest_name):
        print "%s / %s" % (number, len(convertList))
        pass

    print "%s file(s) copied, %s failed" % ExecuteConvertList(dest_path='C:\pictTest', conver_list=convertList,
                                                              sym_link=False, auto_orient=True, incr_callback=_callback)
            
